package org.swift.mashup.engine.deployment;

import java.util.HashMap;
import java.util.Map;

import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * Validates the incoming service XML against either a Pattern XML (Rule)
 * <p> 
 * Example of Pattern XML (Rule):
 * 	<mx:validation>
 * 		<elementOne xmlns:xsd="http://www.w3.org/2001/XMLSchema">
 * 			<elementTwo xsd:minOccurs="1">
 * 				<elementThree>
 * 				</elementThree>
 * 			</elementTwo>
 * 		</elementOne> 
 *  </mx:validation> 
 *  <p>
 *  The Pattern XML (Rule) supports a Matching and Requirement functionality:
 *  Matches = all elements in the user submitted XML exist in validation pattern XML.  If there is 
 *			  at least one element that doesn't exist in the validation pattern XML then the request
 *			  fails.
 *	Required = if an element from the user submitted XML occurs less than the required amount
 *			   indicated in the validation pattern XML the request fails.
 *			   NOTE: as of this release (1.0) the namespace definition for the "xsd"
 *			   must be "xmlns:xsd="http://www.w3.org/2001/XMLSchema".  This could be changed
 *			   but I ran out of time.  This only applies to the validation XML.
 * 
 * This validation code is Namespace aware.  Meaning the namespace URI is used to resolve
 * the validation for each element instead of its prefix id (some_prefix_id:namespace_URI).
 * This allows the validation xml to have different namespace prefixes than the service request
 * XML.
 * 
 * @author Ken Dowd
 * @version 1.0 Feb 7, 2013.
 */
public class MatchesValidationProcess implements ValidationProcess {
	private String NODE_PATH_SEPERATOR		= "/";
	private String REQUIREMENT_INDICATOR	= "xsd:minOccurs";
    private String VALIDATION_FAILED        = "Mashup validation rules failed";
	
	private Map<String,Integer> matchesMap	= null;
	private Map<String,Integer> requiredMap	= null;
	private Map<String,Integer> requestMap	= null;
	
	private static Logger log = Logger.getLogger(MatchesValidationProcess.class);
	
	@Override
	public boolean validateRule(Document requestDOM, Document validationDOM) throws Exception {
		boolean	validated	= false;
		
		if (requestDOM != null && validationDOM != null ) {
			
			matchesMap	= new HashMap<String,Integer>();
			requiredMap	= new HashMap<String,Integer>();
			requestMap	= new HashMap<String,Integer>();
			buildValidationMaps(validationDOM, matchesMap, requiredMap);
			
			//Build a Matches map from the user submitted XML
			buildValidationMaps(requestDOM, requestMap, null);
			
			validated	= validateRules(requestMap, matchesMap, requiredMap);
		} else {
			throw new Exception("The requestDOM (Document) and/or validationDOM (Document) parameters is null");
		}
		
		return validated;
	}
	
	private void buildValidationMaps(Document doc, Map<String,Integer> matchesMap, Map<String,Integer> requiredMap) throws Exception {
		if ( doc != null && matchesMap != null ) {
			mapNodeElement((Node)doc.getDocumentElement(), "", matchesMap, requiredMap);
		} else {
			throw new Exception("The Document and/or Map<String,Integer> parameters is null");
		}
	}
	
	/**
	 * Validates the service call XML against the Matches and Required Maps (derived
	 * from the validation pattern XML.
	 * 
	 * @param  matchesPostMap Map that represents service call XML
	 * @param  matchesRuleMap Map that contains the Matches values from validation pattern XML
	 * @param  requiredRuleMap Map that contains the Required values from the validation pattern XML
	 * @throws Exception if there is some runtime error while validating the XML
	 * @author Ken Dowd
 	 */
	private boolean validateRules(Map<String,Integer> matchesPostMap, Map<String,Integer> matchesRuleMap, Map<String,Integer> requiredRuleMap) throws Exception {
		boolean	bRuleValidated	= true;
		String 	key				= null;
		Integer	value			= null;
		Integer	requiredCnt		= null;
		
		//Loop through the Post data map 
		for (Map.Entry<String, Integer> entry : matchesPostMap.entrySet()) {
		    key = entry.getKey();
		    value = entry.getValue();
		    
		    //validate it against the "Matches" map.  Meaning does the element exist.
		    if ( matchesRuleMap.get(key) == null ) {
		    	//Rule fails
		    	throw new ValidationException(String.format("%s: Unexpected element '%s'", VALIDATION_FAILED, key));
		    }
		    
		    //validate it against the "Required" map.  Meaning does the element occur the minimum amount of times.
		    if ( requiredRuleMap.get(key) != null ) {
		    	requiredCnt	= requiredRuleMap.get(key);
		    	if ( value < requiredCnt.intValue() ) {
		    		//Rule fails
                    throw new ValidationException(String.format("%s: Missing required element '%s'", VALIDATION_FAILED, key));
		    	}
		    }
		}
		
		return bRuleValidated;
	}
	
	/**
	 * Recursive function that populates maps based on the Node provided.
	 *   
	 * The matchesMap will be populated with a key that represents each unique element
	 * name (with the parent path prepended) in the XML structure.
	 * 
	 * The requiredMap will be populated with a key that represents any element that 
	 * had an attribute that indicated it would be required.
	 *   
	 * This function will be called for the Validation Pattern XML and the service call
	 * supplied XML. 
	 * 
	 * @param  node that represents the current node
	 * @param  matchesMap Map that should contain a summation of the elements from the first parameter
	 * @param  requiredMap Map that should contain any requirements (xsd:minOccurs) from the first parameter
	 * @throws Exception if there is some runtime error while walking the Node
	 * @author Ken Dowd
 	 */
	private void mapNodeElement(Node node, String parentPath, Map<String,Integer> matchesMap, Map<String,Integer> requiredMap) throws Exception {
		String	nodeNamespace		= "";
		String	workingParentPath	= null;
		
		if ( node.getNodeType() == Node.ELEMENT_NODE ) {
			if ( node.getNamespaceURI() != null ) {
				nodeNamespace	= node.getNamespaceURI();
			}
			
			workingParentPath	= parentPath + NODE_PATH_SEPERATOR + "<" + nodeNamespace + ">:" + node.getLocalName();
			NodeList	childrenNodes	= null;
			NamedNodeMap	attribs		= null;
			Node			attrib		= null;
			Integer			occurrences	= null;
			
			//Place an entry in the matches map for the base node
			occurrences	= matchesMap.get(workingParentPath);
			if ( occurrences != null ) {
				occurrences++;
			} else {
				occurrences	= new Integer(1);
			}
			matchesMap.put(workingParentPath, occurrences);
			log.info("mapNodeElement() - : matchesMap.put(workingParentPath, occurrences)=" + workingParentPath);
			
			//Test for xsd:minoccurs which denotes a required element
			attribs	= node.getAttributes();
			if ( attribs != null && attribs.getLength() > 0 ) {
				attrib	= attribs.getNamedItem(REQUIREMENT_INDICATOR);
				if ( attrib != null ) {
					requiredMap.put(workingParentPath, Integer.parseInt(attrib.getNodeValue()));
					log.info("mapNodeElement() - : requiredMap.put(workingParentPath, Integer.parseInt(attrib.getNodeValue()))=" + workingParentPath);
				}
			}
			
			//Test if the node has children
			if ( node.hasChildNodes() ) {
				childrenNodes	= node.getChildNodes();
				for (int i = 0; i < childrenNodes.getLength(); i++) {
			        Node currentNode = childrenNodes.item(i);
			        if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
			        	mapNodeElement(currentNode, workingParentPath, matchesMap, requiredMap);
			        }
			    }
			}
		}
	}
}
